﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using System.Net.Sockets;
using System.Net;
using System.Reflection;

namespace WinMPD
{
    public class MPDServer
    {
        /// <summary>
        /// Starts a new server thread.
        /// </summary>
        /// <param name="log">Stream to log to.</param>
        public static Thread Start(TextWriter log)
        {
            MPDServer server = new MPDServer();
            server.logger = log;
            Thread worker = new Thread(server.Run);
            worker.Start();
            return worker;
        }

        /// <summary>
        /// Server's logfile- all messages will be written here.
        /// </summary>
        public TextWriter logger;
        /// <summary>
        /// Server's connection symbol.
        /// </summary>
        private TcpClient handler;
        /// <summary>
        /// All threads currently serving clients.
        /// </summary>
        private List<Thread> clients = new List<Thread>();
        /// <summary>
        /// Running version of the assembly (version number).
        /// </summary>
        private static Version progVersion = Assembly.GetExecutingAssembly().GetName().Version;

        /// <summary>
        /// Handles blocking when accepting new TCP connections.
        /// </summary>
        private ManualResetEvent allDone = new ManualResetEvent(false);
        /// <summary>
        /// Starts the server and listens for connections.
        /// </summary>
        public void Run()
        {
            logger.WriteLine("Starting up WinMPD {0}",progVersion);
            //Create socket for IPv4/TCP
            TcpListener listen = new TcpListener(IPAddress.Any, 6600);
            logger.WriteLine("Bound to {0}.", listen.LocalEndpoint);

            //Listen for incoming connections
            listen.Start();
            logger.WriteLine("Listening for clients");

            while (true)
            {
                allDone.Reset();
                //Listen for new connections, spin off a new threadCallback for each
                listen.BeginAcceptTcpClient(threadCallback, listen);;
                allDone.WaitOne();
            }
        }

        /// <summary>
        /// Callback to handle new client connections.
        /// </summary>
        private void threadCallback(IAsyncResult ar)
        {
            //Accept the incoming connection
            TcpListener listener = (TcpListener)ar.AsyncState;
            handler = listener.EndAcceptTcpClient(ar);
            //Allow main thread to continue waiting
            allDone.Set();
            logger.WriteLine("Accepted connection from {0} in thread {1}.",
                handler.Client.RemoteEndPoint, clients.IndexOf(Thread.CurrentThread));
            NetworkStream iostream = handler.GetStream();
            /* Setting Encoding.UTF8 puts a BOM at the beginning of the string, which
             * freaks out libmpdclient.  Work around it by using an instance of
             * UTF8Encoding which doesn't insert a BOM. */
            StreamWriter sw = new StreamWriter(iostream, new UTF8Encoding(false));
            sw.AutoFlush = true;
            StreamReader sr = new StreamReader(iostream, Encoding.UTF8);

            //Send greeting
            sw.WriteLine("MPD 0.11.0");

            //Deal with the client
            while (true)
            {
                String recvCommand;
                try
                {
                    recvCommand = sr.ReadLine();
                }
                catch
                {
                    logger.WriteLine("Socket error.  Terminating thread.");
                    break;
                }
                if (recvCommand is String)
                {
                    logger.WriteLine(recvCommand);
                    String result = doCommand(recvCommand, handler);
                    logger.WriteLine(result);
                    sw.WriteLine(result);
                }
                else
                    break;
            }

            //Connection closed, clean up the sockets
            try
            {
                CloseClient(handler);
            }
            catch { /*In case the connection died*/ }
        }

        /// <summary>
        /// Shuts down the calling client thread.
        /// </summary>
        private void CloseClient(TcpClient handler)
        {
            logger.WriteLine("Closing connection to {0}.",handler.Client.RemoteEndPoint);
            //logger.WriteLine("Killing server thread {0}.",clients.IndexOf(Thread.CurrentThread));
            Thread.CurrentThread.Abort();
        }

        private string doCommand(string command, TcpClient handler)
        {
            String cmd = command.Split(' ')[0];
            AckError error = AckError.None;
            string errorDetails = "Unspecified error";
            switch (cmd)
            {
                case "ping":
                    break;
                case "close":
                    CloseClient(handler);
                    break;
                default:
                    error = AckError.Unknown;
                    errorDetails = String.Format("unknown command \"{0}\"",cmd);
                    break;
            }
            if (error == AckError.None)
                return "OK";
            return String.Format("ACK [{0}@1]",(byte)error)+" {"+cmd+"} "+errorDetails;
        }

        /// <summary>
        /// Convert a string into a UTF-8 byte array.
        /// </summary>
        private byte[] str2bytes(string p)
        {
            return Encoding.UTF8.GetBytes(p.ToCharArray());
        }
    }
}
